import sys
import io
import os
import time
import re
import hashlib
import httpx
from pathlib import Path
from mcp.server.fastmcp import FastMCP
from datetime import datetime
import execjs
import json
import pygame
from bs4 import BeautifulSoup
import aiohttp
import logging
from urllib.parse import quote

# --- 基础设置与初始化 ---
# 解决中文乱码
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

# 全局配置和状态
GLOBAL_CONFIG = {
    'BILIBILI_COOKIE': "buvid3=93F12A49-F981-A1BA-0AED-15F0D142A79886204infoc; b_nut=1761553986; _uuid=E10F10E877-9EC2-1273-CD94-9D69B1C38D6D87262infoc; enable_web_push=DISABLE; buvid_fp=1fed7b07500755e95e1beb048c9bb0df; buvid4=56A6D30D-9A98-A987-2B51-47CCDC6B257686954-025102716-utYYOJL/XC9Jt+w4cwQwmg%3D%3D; SESSDATA=60eef209%2C1777170984%2C5291e%2Aa2CjA9mXUMkzvODcTYlLOoWzn1kzivtVrpv1umuWM4oQDoh5dzEPcDb4FI9UsEoqGUDfASVk5peV9ISWVvMEtVUENtd2gxRkZfbjNPZ0haLThpbDFSR2RlUXJPVVF6ZXRUMGRZRHo2emRtTWxRWGdKMmRMZDNjZ1RCRjItbVNJa3hRZkxJemlrTVJ3IIEC; bili_jct=897141e18595e2e15d066305bcfbcde7; DedeUserID=214438039; DedeUserID__ckMd5=2ecf106214ccb391; theme-tip-show=SHOWED; rpdid=|(k))k)~~JYl0J'u~YuYkuYYu; CURRENT_QUALITY=0; theme-avatar-tip-show=SHOWED; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzcxNzI2ODAsImlhdCI6MTc3NjkxMzQyMCwicGx0IjotMX0.1a3h2C1c5y2oM4a0r3y7U8lQdG9mS5wWfWfQxXh1x8A; bili_ticket_expires=1777172680; b_lsid=87153B3B_19A520B54AA; sid=4qal6z3f; home_feed_column=4; browser_resolution=925-1063; CURRENT_FNVAL=4048; bp_t_offset_214438039=1131616293565235200" ,
    'NETEASE_MUSIC_U': "0047871AEE16CEAD415D0FFB7CE5D7BFA53428DE257AA0FF066BD789BED042AA9E77BD6F59E8E6E3836E280304CBAFB396C5CCB4A9B76EF8FB9722B690F6C0644B7BE885969D6DBA71B7A5F43C39A6033A6D906E4A2CEBAD7DF7B84FD3B0A57B7B15F4D7E8B1A2065D48F10268E7979DB430554DEF59CD385CDD8D25C994435161F4C1754DC02430E188264029C1F5AF44B2469B00DAA997EB094AF69EC4EF1AE458C1541166FE68BCB79C486AFCD792158CC4A92E62DB85FCC9267CE31C528A94D60D52E04D7F8DD938EFC96C2FA36CB8E35603D539D7879E7DE7816E9FFBF324C9B4212DEDD9418EB5749FF580B8DD6790045939281D724030020CD17F0E02203B6207968ADDF6F0EDCD55C1CF7B7431A77B85BFE0AF1B34972AAC371CC47CDE7174D69ADE01FA4E8FAA4C4196BE73981F452DF975087ADE00BA575E3F4936F51CFE781E56C954687C0340F1961A7075563289FBD0F59751E19A31A9970512D93BA8AC35954EC994158D18E9F31A04897CDB00A498B6BA7629ABE97FC523E46F8549517ED405437D1B6CC7BEDA3F2BD02B39D4D51D4EE10848E376425F64712E",
    'NETEASE_CSRF_TOKEN': "2f6dc3c9033814e59eafb23e8e374727",
    'NETEASE_JS_PATH': 'wyy.js'
}

audio_thread = None
is_playing = False
stop_playing = False
current_file = None

# 日志配置 (全局一次性配置)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# 初始化pygame mixer
try:
    pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=512)
    pygame.mixer.init()
    print("✅ pygame mixer 初始化成功")
except Exception as e:
    print(f"⚠️ pygame mixer 初始化失败: {e}")


def create_mcp_server(port: int = 7888) -> FastMCP:
    """创建并配置MCP服务器，工具函数依赖 GLOBAL_CONFIG"""
    mcp = FastMCP(name="unified_mcp_server", port=port, host="0.0.0.0")

    @mcp.tool()
    async def extract_bilibili_subtitles(bvid: str, prefer_lang: str = 'ai-zh') -> str:
        """通过 BVID 异步查询 Bilibili 视频的 AI 字幕内容。"""
        # 从全局配置中获取 Cookie
        cookie = GLOBAL_CONFIG['BILIBILI_COOKIE']
        if "YOUR_VALID_BILIBILI_COOKIE_HERE" in cookie:
             return "❌ 工具未配置：请在主函数中设置有效的 BILIBILI_COOKIE。"
             
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
            "Referer": "https://www.bilibili.com/",
            "Cookie": cookie
        }
        
        async with aiohttp.ClientSession(headers=headers) as session:
            # --- 1. 获取 AID 和 CID ---
            aid_url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
            async with session.get(aid_url, timeout=10) as resp_aid:
                if resp_aid.status != 200:
                    return f"❌ 步骤 1 失败: 获取 AID 时 HTTP 错误 {resp_aid.status}"
                aid_data = await resp_aid.json()
                if aid_data.get('code') != 0 or not aid_data.get('data'):
                    return f"❌ 步骤 1 失败: AID API 响应错误: {aid_data.get('message', '未知错误')}"
                
                aid = aid_data['data']['aid']
                logging.info(f"-> 成功获取 AID: {aid}")
                
                cid_url = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp"
                async with session.get(cid_url, timeout=10) as resp_cid:
                    if resp_cid.status != 200:
                        return f"❌ 步骤 1 失败: 获取 CID 时 HTTP 错误 {resp_cid.status}"
                    cid_data = await resp_cid.json()
                    if cid_data.get('code') != 0 or not cid_data.get('data') or not cid_data['data']:
                        return f"❌ 步骤 1 失败: CID API 响应错误: {cid_data.get('message', '未知错误')}"
                    cid = cid_data['data'][0]['cid']
                    logging.info(f"-> 成功获取 CID: {cid}")

            # --- 2. 请求 V2 API (获取字幕 URL) ---
            v2_api_url = f"https://api.bilibili.com/x/player/wbi/v2?aid={aid}&cid={cid}"
            async with session.get(v2_api_url, timeout=10) as resp_v2:
                if resp_v2.status != 200:
                    return f"❌ 步骤 2 失败: V2 API 请求错误 {resp_v2.status} (Cookie/WBI 问题)"
                response_v2_text = await resp_v2.text()

            # --- 3. 字幕内容提取与清理 ---
            try:
                data_v2 = json.loads(response_v2_text)
                if data_v2.get('code') != 0:
                    return f"❌ 步骤 3 失败: V2 JSON 响应返回错误代码: {data_v2.get('code')}"

                subtitles = data_v2.get('data', {}).get('subtitle', {}).get('subtitles')
                if not subtitles:
                    return "⚠️ 视频没有找到任何字幕信息。"
                    
                target_sub = next((sub for sub in subtitles if sub.get('lan') == prefer_lang), 
                                  next((sub for sub in subtitles if sub.get('lan') == 'zh'), subtitles[0]))
                
                relative_url = target_sub.get('subtitle_url')
                if not relative_url:
                    return "❌ 步骤 3 失败: 无法提取 'subtitle_url'。"
                    
                ai_subtitle_url = "https:" + relative_url if relative_url.startswith('//') else relative_url

                async with session.get(ai_subtitle_url, timeout=15) as response_sub:
                    response_sub.raise_for_status()
                    subtitle_data = await response_sub.json()

            except Exception as e:
                return f"❌ 步骤 3 失败: 字幕文件请求或解析出错: {e}"

            all_contents = []
            lyric_pattern = re.compile(r'♪.*♪') 

            for item in subtitle_data.get('body', []):
                content = item.get('content')
                if content:
                    cleaned_content = lyric_pattern.sub('', content).strip()
                    if cleaned_content:
                        all_contents.append(cleaned_content)
                    
            final_text = "，".join(all_contents)
            
            if not final_text:
                return "⚠️ 字幕内容为空或清理后无有效文本。"
                
            return f"✅ 成功提取 {bvid} 的字幕：\n{final_text}"

    @mcp.tool()
    async def search_netease_music(keyword: str) -> str:
        """网易云音乐搜索工具: 根据关键词搜索歌曲。"""
        config = GLOBAL_CONFIG
        
        if "YOUR_VALID_MUSIC_U_COOKIE_HERE" in config['NETEASE_MUSIC_U']:
             return "❌ 工具未配置：请在主函数中设置有效的 NETEASE_MUSIC_U Cookie。"

        headers={'referer':'https://music.163.com/', 'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'}
        cookies = {'MUSIC_U': config['NETEASE_MUSIC_U']}
        
        i1x = {
            "hlpretag": "<span class=\"s-fc7\">", "hlposttag": "</span>", "s": keyword, "type": "1",
            "offset": "0", "total": "true", "limit": "5", "csrf_token": config['NETEASE_CSRF_TOKEN']
        }
        url='https://music.163.com/weapi/cloudsearch/get/web'

        try:
            if not Path(config['NETEASE_JS_PATH']).exists():
                return f"错误：找不到加密脚本文件 {config['NETEASE_JS_PATH']}。"
            
            with open(config['NETEASE_JS_PATH'], encoding='utf-8') as f:
                js_code = execjs.compile(f.read())
                r = js_code.call('GetSign', i1x)
                data = {"params": r.get("encText"), "encSecKey": r.get("encSecKey")}

            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(url=url, data=data, headers=headers, cookies=cookies)
                response.raise_for_status()

            result = json.loads(response.text)
            if result.get("code") == 200 and "songs" in result["result"]:
                songs_info = []
                for song in result["result"]["songs"]:
                    privilege = song.get("privilege", {})
                    is_vip = (privilege.get("fee") == 1 and privilege.get("pl") == 0)
                    
                    if not is_vip:
                        singers = ",".join([ar["name"] for ar in song["ar"]])
                        duration = song["dt"] // 1000
                        
                        songs_info.append(f"""
-{'-' * 50}
歌曲名称: {song['name']}
歌曲ID: {song['id']}
歌曲歌手: {singers}
歌曲时长: {duration // 60}:{duration % 60:02d}
是否VIP歌曲: 否
-{'-' * 50}""")
                
                return "\n".join(songs_info) if songs_info else "未找到非VIP歌曲。"
            else:
                return f"搜索失败：{result.get('message', '未知错误')}"
                
        except Exception as e:
            return f"发生未知错误：{type(e).__name__}: {str(e)}"

    @mcp.tool()
    async def parse_weather(city: str, days: int = 1) -> str:
        """查询指定城市的天气信息，返回指定天数（1-14天）的天气预报"""
        days = max(1, min(14, days))
        
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
            'Referer': 'https://www.baidu.com/'
        }

        try:
            url = f"https://weathernew.pae.baidu.com/weathernew/pc?query={quote(f'{city}天气')}&srcid=4982&forecast=long_day_forecast"
            
            async with aiohttp.ClientSession() as session:
                async with session.get(url, headers=headers, timeout=15.0) as resp:
                    resp.raise_for_status()
                    html = await resp.text()
                
            soup = BeautifulSoup(html, 'lxml')
            script_tag = soup.find('script', text=re.compile(r'window\.tplData'))
            
            if not script_tag:
                return "解析失败：未找到天气数据 (网站结构可能已变更)"
                
            tpl_data_match = re.search(r'window\.tplData\s*=\s*({.*})', script_tag.get_text(), re.DOTALL)
            if not tpl_data_match:
                 return "解析失败：未找到 window.tplData JSON 数据"
            
            forecast_data = json.loads(tpl_data_match.group(1))
            forecast = forecast_data.get("long_day_forecast", {}).get("info", [])
            
            if not forecast:
                return f"未获取到{city}的天气预报"
            
            result = []
            for i in range(min(days, len(forecast))):
                data = forecast[i]
                result.append(
                    f"[{'今日' if i == 0 else '第' + str(i+1) + '天'}] "
                    f"日期: {data.get('date', '未知')}, "
                    f"天气: {data.get('weather_day') or data.get('weather', '未知')}, "
                    f"温度: {data.get('temperature_night', '未知')}°C~{data.get('temperature_day', '未知')}°C"
                )
            
            return '\n'.join(result)
        
        except Exception as e:
            return f"天气查询过程中发生内部错误：{type(e).__name__}: {str(e)}"

    @mcp.tool()
    async def get_nowtime() -> str:
        """获取当前时间,格式为: 2025-10-15 10:00:00"""
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    @mcp.tool()
    async def parse_netease_music(music_url: str) -> str:
        """网易云音乐解析接口: 根据链接返回标题与直链。"""
        id_match = re.search(r'(?:/song|\#/song)\?id=(\d+)', music_url)
        if not id_match:
            return "解析失败：无效的网易云音乐链接，未找到歌曲 ID。"

        song_id = id_match.group(1)
        download_url_template = f'https://music.163.com/song/media/outer/url?id={song_id}'

        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            'Referer': 'https://music.163.com',
        }

        try:
            async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
                # 1. 获取标题
                response = await client.get(music_url, headers=headers)
                response.raise_for_status()

                title_match = re.search(r'<title>(.*?)</title>', response.text, re.DOTALL)
                title = title_match.group(1).split('-')[0].strip() if title_match else f'歌曲ID_{song_id}'
                
                # 2. 尝试获取真实的下载链接（HEAD请求）
                final_url = download_url_template
                try:
                    head_response = await client.head(download_url_template, headers=headers)
                    final_url = str(head_response.url)
                    if 'music.163.com' not in final_url:
                        download_url_template = final_url
                except:
                    pass
                
                return (
                    f"标题: {title}\n"
                    f"直链: {download_url_template}\n\n"
                    f"💡 提示：可以使用 download_files 工具直接下载此文件"
                )

        except Exception as e:
            return f"请求失败或解析错误：{type(e).__name__}"

    @mcp.tool()
    async def parse_video_url(video_url: str) -> str:
        """解析抖音或B站的视频链接，返回标题和媒体链接"""
        if not re.search(r'(douyin\.com|bilibili\.com)', video_url):
            return "解析失败：请输入一个有效的抖音或B站视频链接。"

        api_url = 'https://api.snapany.com/v1/extract'
        
        locale = 'zh'
        qc = '6HTugjCXxR'
        timestamp = str(int(time.time() * 1000))
        combined_string = f"{video_url}{locale}{timestamp}{qc}"
        g_footer = hashlib.md5(combined_string.encode('utf-8')).hexdigest()

        headers = {
            'accept': '*/*', 'accept-language': 'zh', 'content-type': 'application/json',
            'origin': 'https://snapany.com', 'referer': 'https://snapany.com/',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
            'g-footer': g_footer, 'g-timestamp': timestamp,
        }
        payload = {'link': video_url}

        try:
            async with httpx.AsyncClient(timeout=30.0) as client:
                response = await client.post(api_url, headers=headers, json=payload)
                response.raise_for_status()
            data = response.json()

            title = data.get('text', '无标题')
            medias = data.get('medias', [])

            if not medias:
                return f"解析成功，但未能找到媒体文件。\n标题: {title}"

            video_info = medias[0]
            result_video_url = video_info.get('resource_url', '未找到')
            preview_url = video_info.get('preview_url', '未找到')
            audio_url = medias[-1].get('resource_url') if len(medias) > 1 else '无音频'

            return (
                f"标题: {title}\n"
                f"视频直链: {result_video_url}\n"
                f"音频直链: {audio_url}\n"
                f"视频封面: {preview_url}"
            )
        except Exception as e:
            return f"解析失败：{type(e).__name__}"

    return mcp

def run_mcp_server(port: int = 7888):
    """运行MCP服务器"""
    print("🚀 启动MCP HTTP服务器")
    print("📌 按 CTRL+C 停止服务")
    
    mcp = create_mcp_server(port)
    
    try:
        mcp.run(transport='streamable-http')
    except KeyboardInterrupt:
        print("\n🛑 服务器已停止")
    except Exception as e:
        print(f"\n❌ 服务器启动失败: {e}")

if __name__ == "__main__":
    run_mcp_server()